package com.tosmart.jorm.impl;

import com.tosmart.jorm.annotation.GenerateMethod;
import com.tosmart.jorm.util.DBAssistant;
import com.tosmart.jorm.util.StringUtil;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.HashSet;
import java.util.Set;

/**
 * Such achieved in two ways to automatically
 * generate a numeric field's value.
 *
 * @author tosmart@gmail.com
 * @see com.tosmart.jorm.impl.JormImpl
 */
public class NumberAutoGenerate {

    public static final String CounterTable =
            "ENTITY_COLUMN_VALUES";
    private static final String ReadCondition =
            "WHERE TABLE_NAME=? AND COLUMN_NAME=?";
    private final static String SQL_CREATE_TABLE =
            String.format(
                    "CREATE TABLE %s(" +
                            "TABLE_NAME VARCHAR(64)," +
                            "COLUMN_NAME VARCHAR(64)," +
                            "NEXT_VALUE NUMERIC(32) NOT NULL," +
                            "PRIMARY KEY (TABLE_NAME,COLUMN_NAME)" +
                            ")",
                    CounterTable
            );
    private final static String SQL_READ_COUNTER =
            String.format(
                    "SELECT NEXT_VALUE " +
                            "FROM %s " +
                            "WHERE TABLE_NAME=? AND COLUMN_NAME=?",
                    CounterTable
            );
    private final static String SQL_INSERT_COUNTER =
            String.format(
                    "INSERT INTO %s" +
                            "(TABLE_NAME,COLUMN_NAME,NEXT_VALUE) " +
                            "VALUES (?, ?, ?)",
                    CounterTable
            );
    private final static String SQL_UPDATE_COUNTER =
            String.format(
                    "UPDATE %s " +
                            "SET NEXT_VALUE=? " +
                            "WHERE TABLE_NAME=? AND COLUMN_NAME=?",
                    CounterTable
            );
    private static final String SQL_SEQ_NEXTVAL_PATTERN =
            "SELECT %s.NEXTVAL FROM DUAL";
    private static final String SQL_SELECT_MAX_PATTERN =
            "SELECT MAX(%s) FROM %s";

    /**
     * Constructor
     *
     * @param connection Database connection,
     *                   Under normal circumstances,
     *                   this parameter introduced by
     *                   the JORM framework.
     */
    public NumberAutoGenerate(Connection connection) {
        this.connection = connection;
    }

    /**
     * Entity type field values to determine the need
     * for automatically generated.
     *
     * @param value current field's value
     * @param field Field instance
     * @return true if need, false otherwise
     */
    public static boolean need(
            Object value, Field field) {

        return (value == null || value.equals(0)) &&
                JormReflect.isAutoGenerateColumn(field);
    }

    /**
     * Two methods were used for numeric field,
     * generates a new value.
     *
     * @param entity Entity instance
     * @param field  Field instance, Which is need
     *               automatic generate.
     * @return the new value generated by JORM framework
     * @throws IllegalAccessException Possible throw by Field setter
     */
    public Long generateValue(Object entity, Field field)
            throws IllegalAccessException {

        GenerateMethod method =
                JormReflect.getAutoGenerateMethod(field);

        if (method.equals(GenerateMethod.Increase)) {
            return generateIncrease(entity, field);
        }
        else if (method.equals(GenerateMethod.Sequence)) {
            return generateUseSequence(entity, field);
        }
        else {
            throw new RuntimeException(
                    "Missing 'autoGenerate' argument of @JormMap");
        }
    }

    /**
     * With JORM framework to achieve the automatic
     * increase in their own way to generate a new
     * field for the numeric value.
     *
     * @param entity Entity instance
     * @param field  Field instance, Which is need
     *               automatic generate.
     * @return the new value generated by JORM framework
     * @throws IllegalAccessException Possible throw by Field setter
     */
    private Long generateIncrease(
            Object entity, Field field)
            throws IllegalAccessException {

        String table = JormReflect.getEntityTable(entity.getClass());
        String column = JormReflect.getOrmMapColumn(field);

        if (StringUtil.isEmpty(table) || StringUtil.isEmpty(column)) {
            throw new RuntimeException(
                    "Can't get table/column Name by JormReflect");
        }

        ensureExists(table, column);
        Long value = nextValue(table, column);
        updateNextValue(table, column, value + 1);

        field.setAccessible(true);
        JormReflect.setTypedFieldValue(entity, field, value);

        return value;
    }

    /**
     * Use Oracle sequence, to generate the next value of
     * the number-field.
     *
     * @param entity Entity instance
     * @param field  Field instance, Which is need
     *               automatic generate.
     * @return the next value Oracle sequence
     * @throws IllegalAccessException Possible throw by Field setter
     */
    private Long generateUseSequence(
            Object entity, Field field)
            throws IllegalAccessException {

        String sequence = JormReflect.getOracleSequence(field);
        if (StringUtil.isEmpty(sequence)) return null;

        Long value = sequenceNextValue(sequence);

        field.setAccessible(true);
        JormReflect.setTypedFieldValue(entity, field, value);

        return value;
    }

    /**
     * JORM framework is generated automatically increase
     * the value of the field, it will generate a system
     * table and into the field in which the name and the
     * next a numeric value.
     * <p/>
     * This method is used to check the system table exists,
     * if does not exist create it.
     * On the foundation, this method also checks whether
     * the relevant field, the presence of a numeric value,
     * if you do not exist, this field is to generate a
     * new record.
     *
     * @param table  system table name
     * @param column name of current column
     */
    private void ensureExists(String table, String column) {

        if (!counterTableExists) {
            DBAssistant dba = new DBAssistant(connection, false);
            if (!dba.existsTable(CounterTable)) {
                dba.execute(SQL_CREATE_TABLE);
            }
            counterTableExists = true;
        }

        if (!counterColumns.contains(table + column)) {
            DBAssistant dba = new DBAssistant(connection, false);
            if (!dba.exists(CounterTable, ReadCondition, table, column)) {
                Long maxValue = currentMaxColumnValue(table, column);
                dba.execute(SQL_INSERT_COUNTER, table, column, maxValue + 1);
            }
            counterColumns.add(table + column);
        }
    }

    /**
     * From the system table to find the corresponding
     * fields in the next one numeric value.
     *
     * @param table  system table name.
     * @param column name of current column.
     * @return next Value of the column in system table
     */
    private Long nextValue(String table, String column) {
        DBAssistant dba = new DBAssistant(connection, false);
        return dba.selectLong(SQL_READ_COUNTER, table, column);
    }

    /**
     * Update the next value of the column in system table.
     *
     * @param table  system table name.
     * @param column name of the current column.
     * @param value  new value of the column in system table.
     */
    private void updateNextValue(
            String table, String column, Long value) {
        DBAssistant dba = new DBAssistant(connection, false);
        dba.execute(SQL_UPDATE_COUNTER, value, table, column);
    }

    /**
     * get the current max value of Entity field, which need
     * automatic generate value.
     *
     * @param table name of the table assigned with Entity class
     * @param column name of current column.
     * @return the max value of column in database
     */
    private Long currentMaxColumnValue(String table, String column) {
        String sql = String.format(SQL_SELECT_MAX_PATTERN, column, table);
        DBAssistant dba = new DBAssistant(connection, false);
        return dba.selectLong(sql);
    }

    /**
     * Read the Oracle sequence's 'nextval'
     * @param sequence name of oracle sequence
     * @return the sequence.nextval
     */
    private Long sequenceNextValue(String sequence) {

        String sql = String.format(
                SQL_SEQ_NEXTVAL_PATTERN,
                sequence
        );

        DBAssistant dba = new DBAssistant(connection, false);
        return dba.selectLong(sql);
    }

    private Connection connection;

    private static boolean counterTableExists = false;
    private static Set<String> counterColumns = new HashSet<String>();
}
